/**
 * XML Parser written by Maurizio Rigamonti, DIVA Group, University of Fribourg
 * Adapted by Bruno Dumas, DIVA Group, University of Fribourg
 * 
 *
 * Copyright (C) 2009 Bruno Dumas (bruno.dumas -at- unifr.ch), DIVA group, University of Fribourg, Switzerland
 * 
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */


package it.unina.lab.xml;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;

import org.xml.sax.InputSource;

public class XMLDocumentReader{
  public final static String UTF_ENCODING = "UTF-8";
  //special characters
  protected final static int EOF = -1;
  protected final static int SPACE = 32;
  protected final static int CR = 13;
  protected final static int LF = 10;
  protected final static int TAB = 9;
  protected final static int QUOTE = 34;
  protected final static int GREATER = 62;
  protected final static int LESS = 60;
  protected final static int EXCLAMATION = 33;
  protected final static int INTERROGATION = 63;
  protected final static int MINUS = 45;
  protected final static int SLASH = 47;
  protected final static int AMPERSAND = 38;
  protected final static int SEMICOLON = 59;
  protected final static int COLON = 58;
  protected final static int EQUALS = 61;
  protected final static int UNDERSCORE = 95;
  //states
  protected final static int START = 0;
  protected final static int ELEMENT_OPEN = 1;
  protected final static int ELEMENT_NAME = 2;
  protected final static int ELEMENT_INNER = 3;
  protected final static int ELEMENT_UNDEF = 4;
  protected final static int ELEMENT_CLOSE = 5;
  protected final static int ELEMENT_CLOSE_NAME = 6;
  protected final static int ATTRIBUTES = 7;
  protected final static int ATTRIBUTE_NAME = 8;
  protected final static int ATTRIBUTE_EQUALS = 9;
  protected final static int ATTRIBUTE_OPEN_VALUE = 10;
  protected final static int ATTRIBUTE_VALUE = 11;
  protected final static int HEADER = 12;
  protected final static int COMMENT = 13;
  protected final static int CDATA = 14;
  //fields
  private InputStreamReader reader = null;
  private String filename;
  private Element document;
  private int line = 1;
  private int column = 1;
  private int tagger = 0;
  private String encoding = UTF_ENCODING;

  public XMLDocumentReader(String filename) throws MalformedXMLException, FileNotFoundException, IOException, HephaisTKParsingException{
    this.filename = filename;
    //create file
    try{ //... to read the file from the .jar file
    	InputStream file =  getClass().getResourceAsStream(filename);
    	reader = new InputStreamReader(file, encoding);
    }
    catch(NullPointerException e){	//if null, then maybe it's a casual file
    	FileInputStream file = new FileInputStream(filename);
    	reader = new InputStreamReader(file, encoding);
    }
    if(reader == null){			//then both loading strategies fucked up -.-
    	throw new IOException("couldn't load XML file from following filename : "+filename);
    }
    //create document
    createDocument();
    reader = null;
  }
  
  
  public XMLDocumentReader(InputStream is)throws MalformedXMLException, FileNotFoundException, IOException, HephaisTKParsingException {
	  reader = new InputStreamReader(is, encoding);
	  if(reader == null){			
	    	throw new IOException("couldn't load XML file from following filename : "+filename);
	    }
	  
	//create document
	    createDocument();
	    reader = null;
  }

  public XMLDocumentReader(URL filenameURL) throws MalformedXMLException, FileNotFoundException, IOException, HephaisTKParsingException{
    this(filenameURL.getFile());
  }

  @SuppressWarnings("unused")
private void createDocument() throws MalformedXMLException, IOException, HephaisTKParsingException{
    int c = EOF;
    int state = START;
    StringBuffer word = new StringBuffer();
    String namespace = null;
    //stack for non-terminated elements
    ArrayList<Element> stack = new ArrayList<Element>();
    String attributeName = "";

    main: while(true){
      c = read(c);
      if (c == EOF) {
		break;
	}
      switch(state){
        case START:
          if (c == LESS) {
			state = ELEMENT_OPEN;
		} else if (!isBlank(c)){
            String message = filename + ": invalid start of document. '<' expected, " + (char)c + " found";
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ELEMENT_OPEN:
          if (isCharacter(c)){
            state = ELEMENT_NAME;
            word.append((char)c);
          }else if (c == INTERROGATION) {
			state = HEADER;
		} else if (c == EXCLAMATION) {
			state = COMMENT;
		} else if (!isBlank(c)){
            String message = filename + ": invalid element. '?', '!' or <name> expected, " + (char)c + " found";
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ELEMENT_NAME:
          if (isCharacter(c) || isDigit(c)) {
			word.append((char)c);
		} else if (c == GREATER){
            state = ELEMENT_INNER;
            createElement(namespace, word, stack);
          }else if (isBlank(c)){
            state = ATTRIBUTES;
            createElement(namespace, word, stack);
          }else if (c == SLASH){
            state = ELEMENT_CLOSE;
            createElement(namespace, word, stack);
          }else if (c == COLON){
        	  namespace = word.toString();
        	  clear(word);
          }else{
            String message = filename + ": invalid name for element " + word.toString() + (char)c;
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ELEMENT_CLOSE:
          if (c == GREATER){
            state = ELEMENT_INNER;
            stack.remove(stack.size() - 1);
          }else if (!isBlank(c)){
            String message = filename + ": invalid end of element. '>' expected, " + (char)c + " found";
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ATTRIBUTES:
          if (isCharacter(c)){
            state = ATTRIBUTE_NAME;
            word.append((char)c);
          }else if (c == GREATER){
            state = ELEMENT_INNER;
          }else if (c == SLASH) {
			state = ELEMENT_CLOSE;
		} else if (!isBlank(c)){
            String message = filename + ": invalid syntax of element " + stack.get(stack.size() - 1).getName();
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ATTRIBUTE_NAME:
          if (isCharacter(c)) {
			word.append((char)c);
		} else if (c == EQUALS) {
			state = ATTRIBUTE_OPEN_VALUE;
		} else if (isBlank(c)) {
			state = ATTRIBUTE_EQUALS;
		}else if (c == COLON){
			namespace = word.toString();
      	  	clear(word);
		} else{
            String message = filename + ": invalid attribute name " + attributeName + (char)c + " found";
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ATTRIBUTE_EQUALS:
          if (c == EQUALS) {
			state = ATTRIBUTE_OPEN_VALUE;
		} else if (!isBlank(c)){
            String message = filename + ": invalid syntax of attribute " + attributeName;
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ATTRIBUTE_OPEN_VALUE:
          if (c == QUOTE){
            state = ATTRIBUTE_VALUE;
            attributeName = word.toString();
            clear(word);
          }else if (!isBlank(c)){
            String message = filename + ": invalid syntax of attribute " + attributeName;
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ATTRIBUTE_VALUE:
          if (c == QUOTE){
            state = ATTRIBUTES;
            Attribute attribute = new Attribute(attributeName, word.toString(), tagger++);
            attribute.setNamespace(namespace);
            namespace = null;
            stack.get(stack.size() - 1).addAttribute(attribute);
            clear(word);
          } else {
			word.append((char)c);
		}
          break;
        case ELEMENT_INNER:
          if (c == LESS) {
			state = ELEMENT_UNDEF;
		} else if (!isBlank(c)){
            state = CDATA;
            word.append((char)c);
          }
          break;
        case ELEMENT_UNDEF:
          if (isCharacter(c)){
            state = ELEMENT_NAME;
            word.append((char)c);
          }else if (c == SLASH) {
			state = ELEMENT_CLOSE_NAME;
		} else if (c == INTERROGATION) {
			state = HEADER;
		} else if (c == EXCLAMATION) {
			state = COMMENT;
		} else if (!isBlank(c)){
            String message = filename + ": invalid element. '?', '!' or <name> expected, " + (char)c + " found";
            throw new MalformedXMLException(message, line, column);
          }
          break;
        case ELEMENT_CLOSE_NAME:
          if (c == GREATER){
            state = ELEMENT_INNER;
            int index = stack.size() - 1;
            String name = stack.get(index).getName();
            if (!name.equals(word.toString()) && !(stack.get(index).getNamespace() + ":" + name).equals(word.toString())){
              String message = filename + ": invalid element closure. " + name + " expected, " + word.toString() + " found";
              throw new MalformedXMLException(message, line, column);
            }
            clear(word);
            stack.remove(index);
          } else {
			word.append((char)c);
		}
          break;
        case CDATA:
          if (c == LESS){
            state = ELEMENT_UNDEF;
            int index = stack.size() - 1;
            stack.get(index).addCData(new CData(word.toString(), tagger++));
            clear(word);
          } else {
			word.append((char)c);
		}
          break;
        case HEADER:
          String w = "";
          while(c != GREATER){
            c = read(c);
            w += (char)c;
          }
          int index = w.indexOf("encoding=\"");
          if (index > 1){
            w = w.substring(index + "encoding=\"".length());
            index = w.indexOf("\"");
            if (index > - 1){
              w = w.substring(0, index);
              //if invalid encoding restart
              if (!encoding.equalsIgnoreCase(w)){
                encoding = w;
                //create file
                FileInputStream file = new FileInputStream(filename);
                reader = new InputStreamReader(file, encoding);
                //create document
                createDocument();
                return;
              }
            }
          }
          state = ELEMENT_INNER;
          break;
        case COMMENT:
        	int lastc = ' ';
          while(c != GREATER || lastc != '-') {
           lastc = c;
			c = read(c);
		}
          state = ELEMENT_INNER;
          break;
      }
    }
  }

  private void createElement(String namespace, StringBuffer word, ArrayList<Element> elementsStack) throws HephaisTKParsingException{
      //Element element = new Element(word.toString(), tagger++);
	  Element element = XmlElementCreation.getElClass(word.toString(), tagger++);
	  if(namespace != null){
		  element.setNamespace(namespace);
	  }
      if (document == null) {
		document = element;
	} else {
		elementsStack.get(elementsStack.size() - 1).addElement(element);
	}
      elementsStack.add(element);
      namespace = null;
      clear(word);
  }

  private void clear(StringBuffer word){
    word.delete(0, word.length());
  }

  private int read(int previous) throws IOException{
    int c = reader.read();
    if (c == CR || (c == LF && previous != CR)){
      column = 1;
      line++;
    } else {
		column++;
	}
    return c;
  }

  private boolean isBlank(int c){
    return c == SPACE || c == CR || c == LF || c == TAB;
  }

  private boolean isDigit(int c){
    return c >= '0' && c <= '9';
  }

  private boolean isCharacter(int c){
    return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c== UNDERSCORE || c == MINUS; 
  }

  public Element getXMLDocument(){
    return document;
  }
}

